/*
 * XUtils.java, created on Oct 20, 2005
 * 
 * Copyright (c) 2005 - 2007 Devon Carew
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
 * file except in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
 * ANY KIND, either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package org.xfc.util;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.image.CropImageFilter;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageFilter;
import java.awt.image.ImageProducer;
import java.awt.image.RGBImageFilter;
import java.lang.reflect.InvocationTargetException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.TimeZone;

import javax.swing.GrayFilter;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;

// TODO: write a stable sort algorithim



/**
 * 
 * 
 * @author Devon Carew
 */
public class XUtils
{
    /** DateFormat that formats a date into RFC 1123 format. */
    public static final DateFormat RFC_1123_DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
    
    static
    {
        RFC_1123_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
    }
    
	private XUtils()
	{
		// don't instantiate this -
		
	}
    
    /**
	 * Returns the string for the package of the given class, i.e. "java.lang".
	 * 
     * @param clazz
	 * @return Returns the string for the package of the given class, i.e. "java.lang".
     */
    public static String getPackageName(Class clazz)
    {
        String className = clazz.getName();
        
        int index = className.lastIndexOf('.');
        
        return index == -1 ? null : className.substring(0, index);
    }
    
    /**
	 * Returns the short name of the given class, i.e. "java.lang.String" --&gt; "String".
	 * 
     * @param clazz
	 * @return Returns the short name of the given class, i.e. "java.lang.String" --&gt; "String".
     */
    public static String getShortName(Class clazz)
    {
        String className = clazz.getName();
        
        int index = className.lastIndexOf('.');
        
        return index == -1 ? className : className.substring(index + 1);
    }
    
	/**
	 * Returns the result of calling the object's equals methods, properly handling nulls.
	 * 
	 * @param object1
	 * @param object2
	 * @return Returns the result of calling the object's equals methods, properly handling nulls.
	 */
	public static boolean safeEquals(Object object1, Object object2)
	{
		if (object1 == object2)
			return true;
		
		if (object1 == null || object2 == null)
			return false;
		
		return object1.equals(object2);
	}
    
    /**
     * Center the window within the screen.
     * 
     * @param window the window to center
     */
    public static void centerWindow(Window window)
    {
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        
        Dimension size = window.getSize();
        
        int y = (screenSize.height - size.height) / 2;
        int x = (screenSize.width - size.width) / 2;
        
        window.setLocation(x, y);
    }
    
    /**
     * Center the given window within the bounds of another window. The dialog is
     * positioned vertically on the golden mean of the parent window.
     * 
	 * @param parent the window to center within
     * @param window the window to center
     */
    public static void centerWindow(Window parent, Window window)
    {
        Dimension parentSize = parent.getSize();
        Dimension size = window.getSize();
        Point parentLoc = parent.getLocationOnScreen();
        
        // calculate the difference in heights
        int h = parentSize.height - size.height;
        
        // golden mean
        int n = (int) (h - (h / 1.618));
        
        // If our dialog hangs over the bottom, move it up by half the overhang height.
        if (n + size.height > parentSize.height)
        {
            int diff = n + size.height - parentSize.height;
            
            n -= diff / 2;
        }
        
        // int y = ((parentSize.height - size.height) / 3) + parentLoc.y;
        int x = ((parentSize.width - size.width) / 2) + parentLoc.x;
        
        window.setLocation(x, parentLoc.y + n);
    }
    
    /**
     * Given a string in the form "x,y,width,height" return a Rectangle.
     * 
	 * @param string a string representation of a Rectangle, in the form "x,y,width,height"
     * @return the Rectangle cooresponding to the given string
     */
    public static Rectangle convertToRectangle(String string)
    {
        if (string == null || string.length() == 0)
            return null;
        
        String[] values = string.split(",");
        
		return new Rectangle(
			Integer.parseInt(values[0]), Integer.parseInt(values[1]),
			Integer.parseInt(values[2]), Integer.parseInt(values[3]));
    }
    
    /**
     * 
     * 
     * @param rect
     * @return a String representing the given Rectangle
     */
    public static String convertToString(Rectangle rect)
    {
        if (rect == null)
            return null;
        else
            return rect.x + "," + rect.y + "," + rect.width + "," + rect.height;
    }
    
    /**
	 * Returns the XFC major version.
	 * 
	 * @return Returns the XFC major version.
	 */
	public static double getXFCVersion()
	{
		return 0.4d;
	}
	
	/**
	 * Returns the current Java version. Examples are 1.4, 1.5, and 1.6.
	 * 
	 * @return Returns the current Java version.
	 */
	public static double getMajorJavaVersion()
	{
		try
		{
			// java.version=1.4.2_12
			String version = System.getProperty("java.version");
			
			int index = version.lastIndexOf('.');
			
			return Double.parseDouble(index == -1 ? version : version.substring(0, index));
		}
		catch (NumberFormatException nfe)
		{
			return 0.0d;
		}
	}
	
	/**
	 * Returns the minor Java version. For example, on Java 1.4.2 this method would return 2.
	 * 
	 * @return Returns the minor Java version.
	 */
	public static int getMinorJavaVersion()
	{
		try
		{
			// java.version=1.4.2_12
			String version = System.getProperty("java.version");
			
			if (version.indexOf('-') != -1)
				version = version.substring(0, version.indexOf('-'));
			
			if (version.indexOf('_') != -1)
				version = version.substring(0, version.indexOf('_'));
			
			String[] values = version.split("\\.");
			
			if (values.length >= 3)
				return Integer.parseInt(values[2]);
			
			return 0;
		}
		catch (NumberFormatException nfe)
		{
			return 0;
		}
	}
    
	/**
	 * Returns the bounds for the current virtual display. The virtual display is the composite
	 * of all the physical displays.
	 * 
	 * @return Returns the bounds for the current virtual display.
	 */
	public static Rectangle getVirtualDisplayBounds()
	{
		Rectangle 			bounds = new Rectangle();
		GraphicsDevice[] 	devices = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
		
		for (int i = 0; i < devices.length; i++)
		{ 
			GraphicsConfiguration graphicsConfiguration = devices[i].getDefaultConfiguration();
			
			bounds = bounds.union(graphicsConfiguration.getBounds());
		}
		
		return bounds;
	}
	
	
    /**
     * Breaks the given ImageIcon into an array of smaller ImageIcons. Each resulting
     * ImageIcon has a width and height equal to the specified parameters. It is assumed that the
     * spcified master ImageIcon is a horizontal strip of images, each <code>width</code> wide.
     * 
     * @param icon
     * @param width
     * 
     * @return Returns the result of breaking the given ImageIcon into a row of smaller icons.
     */
    public static ImageIcon[] splitImageIconStrip(ImageIcon icon, int width)
    {
    	ImageIcon[][] result = splitImageIconArray(icon, width, icon.getIconHeight());
    	
    	int count = icon.getIconWidth() / width;
    	
    	ImageIcon[] temp = new ImageIcon[count];
    	
    	for (int i = 0 ; i < count; i++)
    		temp[i] = result[i][0];
    	
    	return temp;
    }
    
    /**
     * Breaks the given ImageIcon into a 2-dimensional array of smaller ImageIcons. Each resulting
     * ImageIcon has a width and height equal to the specified parameters. It is assumed that the
     * spcified master ImageIcon is an even multiple of the smaller width and height. The proper way
     * to index the results are results[x pos][y pos].
     * 
     * @param icon
     * @param width
     * @param height
     * 
     * @return Returns the result of breaking the given ImageIcon into an array of smaller icons.
     */
    public static ImageIcon[][] splitImageIconArray(ImageIcon icon, int width, int height)
    {
    	int masterWidth = icon.getIconWidth();
    	int masterHeight = icon.getIconHeight();
    	
    	ImageIcon[][] result = new ImageIcon[masterWidth / width][masterHeight / height];
    	
    	for (int x = 0; x < masterWidth; x += width)
    	{
    		for (int y = 0; y < masterHeight; y += height)
    		{
    			ImageFilter 	cropFilter = new CropImageFilter(x, y, width, height);
    			ImageProducer 	imageProducer = new FilteredImageSource(icon.getImage().getSource(), cropFilter);
    	        Image 			image = Toolkit.getDefaultToolkit().createImage(imageProducer);
    	        
    			result[x / width][y / height] = new ImageIcon(image);
    		}
    	}
    	
    	return result;
    }
    
    /**
     * Returns the given text with the first letter capitolized.
     * 
     * @param text
     * @return Returns the given text with the first letter capitolized.
     */
    public static String capitalize(String text)
    {
    	if (text == null || text.length() == 0)
    		return text;
    	
    	String upper = Character.toString(Character.toTitleCase(text.charAt(0)));
    	
    	if (text.length() == 1)
    		return upper;
    	else
    		return upper + text.substring(1);
    }
    
    /**
     * @return Returns a few sentences of sample text.
     */
    public static String getLoremIpsumText()
    {
		return "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "
			+ "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " 
			+ "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " 
			+ "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
	}
    
    /**
     * Returns sample text. The method will return approximately the amount of chars you ask for. The
     * text will always be terminated by a period and will have no leading or trailing spaces.
     * 
     * @param charCount
     * @return Returns sample text.
     */
    public static String getLoremIpsumText(int charCount)
    {
    	String lorem = getLoremIpsumText();
    	
    	while (lorem.length() < charCount)
    		lorem += " \n" + getLoremIpsumText();
    	
    	lorem = lorem.substring(0, charCount - 1).trim();
    	
    	if (!lorem.endsWith("."))
    		lorem += ".";
    	
    	return lorem;
    }
    
    /**
     * Throw an UnsupportedOperationException with the name of the calling method.
     */
    public static void throwNotImplemented()
    {
    	String message = "Not implemented";
    	
    	StackTraceElement[] stackTrace = new Throwable().getStackTrace();
    	
    	if (stackTrace == null || stackTrace.length <= 1)
        	throw new UnsupportedOperationException(message);
    	else
        	throw new UnsupportedOperationException(message + ": " + stackTrace[1].toString());
    }
    
    public static Icon createDisabledIcon(ImageIcon icon)
	{
		return new ImageIcon(GrayFilter.createDisabledImage(icon.getImage()));
	}
	
	public static Icon createHoverIcon(ImageIcon icon)
	{
		// TODO: this effect could be improved -
		ImageFilter		filter = new LighterFilter(0.20);
		ImageProducer 	imageProducer = new FilteredImageSource(icon.getImage().getSource(), filter);
        Image 			image = Toolkit.getDefaultToolkit().createImage(imageProducer);
        
		return new ImageIcon(image);
	}
	
	/**
	 * Returns the file extension for the given file name, or null if there is no extension. For
	 * example, "sample.txt" would return "txt", and "MISC.XLS" would return "XLS".
	 * 
	 * @param fileName the file name
	 * @return the file extension for the given file name, or null if there is no extension
	 */
	public static String getFileExtension(String fileName)
	{
		int index = fileName.lastIndexOf('.');
		
		if (index != -1)
			return fileName.substring(index + 1);
		
		return null;
	}
	
	
	private static class LighterFilter
		extends RGBImageFilter
	{
		private double percentage;
		
		public LighterFilter(double percentage)
		{
			if (percentage < 0.0 || percentage > 1.0)
				throw new IllegalArgumentException();
			
			this.percentage = percentage;
		}
		
		public int filterRGB(int x, int y, int rgb)
		{
			int a = 0xFF & (rgb >> 24);
			int r = 0xFF & (rgb >> 16);
			int g = 0xFF & (rgb >>  8);
			int b = 0xFF & (rgb      );
			
			r = Math.max(0, Math.min(255, r + (int)(r * percentage)));
			g = Math.max(0, Math.min(255, g + (int)(g * percentage)));
			b = Math.max(0, Math.min(255, b + (int)(b * percentage)));
			
			return (a << 24) | (r << 16) | (g << 8) | b;
		}
	}
	
	public static Icon createPressedIcon(final Icon icon)
	{
		return new Icon() {
			public int getIconHeight() {
				return icon.getIconHeight();
			}
			public int getIconWidth() {
				return icon.getIconWidth();
			}
			public void paintIcon(Component c, Graphics g, int x, int y) {
				// TODO: use sub-pixel positioning for a more subtle effect?
				
				icon.paintIcon(c, g, x + 1, y + 1);
			}
		};
	}
	
	public static Icon decorateIcon(Icon icon, Icon decoration)
	{
		return decorateIcon(icon, decoration, SwingConstants.CENTER, SwingConstants.CENTER);
	}
    
	public static Icon decorateIcon(final Icon icon, final Icon decoration, final int hAlignment, final int vAlignment)
	{
		return new Icon()
		{
			public int getIconHeight()
			{
				return icon.getIconHeight();
			}
			
			public int getIconWidth()
			{
				return icon.getIconWidth();
			}

			public void paintIcon(Component component, Graphics g, int x, int y)
			{
				// Paint the main icon.
				icon.paintIcon(component, g, x, y);
				
				// Paint the decorator.
				int hslop = icon.getIconWidth() - decoration.getIconWidth();
				int vslop = icon.getIconHeight() - decoration.getIconHeight();
				
				int xadd = 0;
				int yadd = 0;
				
				if (hAlignment == SwingConstants.LEFT)
					xadd = 0;
				else if (hAlignment == SwingConstants.RIGHT)
					xadd = hslop;
				else
					xadd = hslop / 2;
				
				if (vAlignment == SwingConstants.TOP)
					yadd = 0;
				else if (vAlignment == SwingConstants.BOTTOM)
					yadd = vslop;
				else
					yadd = vslop / 2;
				
				decoration.paintIcon(component, g, x + xadd, y + yadd);
			}
		};
	}
	
	/**
	 * The same as calling Class.forName(), except instead of throwing a ClassNotFoundException,
	 * this method returns null.
	 * 
	 * @param className
	 *            the class to return
	 * @return the Class or null if it is not found
	 */
	public static Class classForName(String className)
	{
		try
		{
			return Class.forName(className);
		}
		catch (ClassNotFoundException cnfe)
		{
			return null;
		}
	}
	
	/**
	 * A direct pass through to SwingUtilities.invokeLater(). Included here for completeness.
	 * 
	 * @param runnable
	 */
	public static void invokeLater(Runnable runnable)
	{
		SwingUtilities.invokeLater(runnable);
	}
	
	/**
	 * Invokes the runnable's run() method on the event dispatch thread. If the current thread is
	 * the EDT, the runnable.run() method is called directly.
	 * 
	 * @param runnable
	 */
	public static void invokeAndWait(Runnable runnable)
	{
		if (SwingUtilities.isEventDispatchThread())
		{
			runnable.run();
		}
		else
		{
			try
			{
				SwingUtilities.invokeAndWait(runnable);
			}
			catch (InterruptedException e)
			{
				throw new RuntimeException(e);
			}
			catch (InvocationTargetException e)
			{
				throw new RuntimeException(e);
			}
		}
	}
	
	/**
	 * Invokes the runnable's run() method on the event dispatch thread. If the current thread is
	 * the EDT, the runnable.run() method is called directly. Returns the result of calling
	 * runnable.run().
	 * 
	 * @param runnable
	 * @return the result of calling runnable.run()
	 */
	public static Object invokeAndWait(final XResultRunnable runnable)
	{
		final Object[] 				result = new Object[1];
		final RuntimeException[] 	exception = new RuntimeException[1];
		
		if (SwingUtilities.isEventDispatchThread())
		{
			return runnable.run();
		}
		else
		{
			try
			{
				SwingUtilities.invokeAndWait(new Runnable() {
					public void run()
					{
						try
						{
							result[0] = runnable.run();
						}
						catch (RuntimeException re)
						{
							exception[0] = re;
						}
					}
				});
			}
			catch (InterruptedException e)
			{
				throw new RuntimeException(e);
			}
			catch (InvocationTargetException e)
			{
				throw new RuntimeException(e);
			}
		}
		
		if (exception[0] != null)
			throw exception[0];
		
		return result[0];
	}
	
	/**
	 * Sleep the indicated number of milliseconds.
	 * 
	 * @param millis the milliseconds to sleep
	 */
	public static void sleepMillis(int millis)
	{
		try
		{
			Thread.sleep(millis);
		}
		catch (InterruptedException e)
		{
			throw new RuntimeException(e);
		}
	}
	
}
